package jxtend.processor.eclipse.visitor.handler.impl;

import jxtend.processor.eclipse.utils.EclipseProcessorUtils;
import jxtend.processor.eclipse.visitor.handler.ASTNodeHandlerVisitorContext;
import jxtend.processor.eclipse.visitor.handler.AbstractASTNodeVisitorHandler;

import org.eclipse.jdt.internal.compiler.ast.ASTNode;
import org.eclipse.jdt.internal.compiler.ast.AllocationExpression;
import org.eclipse.jdt.internal.compiler.ast.Annotation;
import org.eclipse.jdt.internal.compiler.ast.CharLiteral;
import org.eclipse.jdt.internal.compiler.ast.Expression;
import org.eclipse.jdt.internal.compiler.ast.FieldDeclaration;
import org.eclipse.jdt.internal.compiler.ast.MessageSend;
import org.eclipse.jdt.internal.compiler.ast.MethodDeclaration;
import org.eclipse.jdt.internal.compiler.ast.QualifiedTypeReference;
import org.eclipse.jdt.internal.compiler.ast.ReturnStatement;
import org.eclipse.jdt.internal.compiler.ast.SingleNameReference;
import org.eclipse.jdt.internal.compiler.ast.SingleTypeReference;
import org.eclipse.jdt.internal.compiler.ast.Statement;
import org.eclipse.jdt.internal.compiler.ast.StringLiteral;
import org.eclipse.jdt.internal.compiler.ast.ThisReference;
import org.eclipse.jdt.internal.compiler.ast.TypeDeclaration;
import org.eclipse.jdt.internal.compiler.classfmt.ClassFileConstants;
import org.eclipse.jdt.internal.compiler.lookup.ExtraCompilerModifiers;
import org.eclipse.jdt.internal.compiler.lookup.Scope;

@SuppressWarnings("restriction")
public class ToStringHandler extends AbstractASTNodeVisitorHandler {

	@Override
	public void beginVisit(ASTNodeHandlerVisitorContext context, ASTNode node, Scope scope) {
		if (!(node instanceof QualifiedTypeReference)
			    || !EclipseProcessorUtils.compoundNameEquals(
			    		((QualifiedTypeReference) node).tokens, jxtend.annotation.keyword.toString.class)
				|| !(context.getParentNode(0) instanceof Annotation)
				|| !(context.getParentNode(1) instanceof TypeDeclaration)) {
			return;
		}

		TypeDeclaration typeDeclaration = (TypeDeclaration) context.getParentNode(1);
		Annotation equalsAndHashCodeAnnotation = (Annotation) context.getParentNode(0);

		appendToStringMethod(context, typeDeclaration, equalsAndHashCodeAnnotation, scope);
	}

	private static void appendToStringMethod(ASTNodeHandlerVisitorContext context, TypeDeclaration typeDeclaration, 
			Annotation toStringAnnotation, Scope scope) {
		if ((typeDeclaration.fields == null) || (typeDeclaration.fields.length == 0)) {
			return;
		}

		long annotationPos =
				EclipseProcessorUtils.encodePos(toStringAnnotation.sourceStart, toStringAnnotation.sourceEnd);

		MethodDeclaration methodDeclaration = new MethodDeclaration(typeDeclaration.compilationResult);
		methodDeclaration.modifiers = ClassFileConstants.AccPublic;
		methodDeclaration.returnType = new SingleTypeReference("String".toCharArray(), annotationPos);
		methodDeclaration.selector = "toString".toCharArray();

		methodDeclaration.declarationSourceStart =
			methodDeclaration.sourceStart =
				methodDeclaration.modifiersSourceStart =
					methodDeclaration.bodyStart =
							toStringAnnotation.sourceStart;
		methodDeclaration.declarationSourceEnd =
			methodDeclaration.sourceEnd =
					toStringAnnotation.sourceEnd;

		methodDeclaration.annotations = new Annotation[] { OverrideAnnotationUtils.createJavaLangOverrideAnnotation(
					context, toStringAnnotation.sourceStart, toStringAnnotation.sourceEnd, scope) };

		ThisReference thisReference = new ThisReference(toStringAnnotation.sourceStart, toStringAnnotation.sourceEnd);

		MessageSend getClassMessageSend = createMessageSend(toStringAnnotation, thisReference, "getClass");
		MessageSend getNameOnGetClassMessageSend = createMessageSend(toStringAnnotation,
					getClassMessageSend, "getSimpleName");

		AllocationExpression allocationExpression = new AllocationExpression();
		allocationExpression.sourceStart = toStringAnnotation.sourceStart;
		allocationExpression.sourceEnd = toStringAnnotation.sourceEnd;
		allocationExpression.type = new SingleTypeReference("StringBuilder".toCharArray(), annotationPos);
		allocationExpression.arguments = new Expression[] { getNameOnGetClassMessageSend };

		MessageSend appendAtCharMessage = createMessageSend(toStringAnnotation,
					allocationExpression, "append", createCharLiteral('@', toStringAnnotation));

		MessageSend identityHashCodeMessage = createMessageSend(toStringAnnotation,
					new SingleNameReference("System".toCharArray(), annotationPos), "identityHashCode", thisReference);

		MessageSend appendIdentityHashCodeMessage = createMessageSend(toStringAnnotation,
					appendAtCharMessage, "append", identityHashCodeMessage);

		MessageSend appendOpeningParenthesesMessage = createMessageSend(toStringAnnotation,
					appendIdentityHashCodeMessage, "append", createCharLiteral('(', toStringAnnotation));

		MessageSend lastAppendMessage = appendOpeningParenthesesMessage;

		for (int i = 0; i < typeDeclaration.fields.length; i++) {
			FieldDeclaration fieldDeclaration = typeDeclaration.fields[i];

			if (fieldDeclaration.isStatic()) { continue; }

			MessageSend appendFieldNameMessage = createMessageSend(toStringAnnotation,
						lastAppendMessage, "append", createStringLiteral(fieldDeclaration.name, toStringAnnotation));

			MessageSend appendIsCharAppend = createMessageSend(toStringAnnotation,
						appendFieldNameMessage, "append", createCharLiteral('=', toStringAnnotation));

			MessageSend appendFieldNameReferenceMessage = createMessageSend(toStringAnnotation,
						appendIsCharAppend, "append", new SingleNameReference(fieldDeclaration.name, annotationPos));

			if (i < (typeDeclaration.fields.length - 1)) {
	   		    MessageSend commaAndSpaceAppendMessageSend = createMessageSend(toStringAnnotation,
	   		    			appendFieldNameReferenceMessage, "append", createStringLiteral(", ", toStringAnnotation));

	   		    lastAppendMessage = commaAndSpaceAppendMessageSend;
			} else {
				lastAppendMessage = appendFieldNameReferenceMessage;
			}
		}

		MessageSend appendClosingParenthesesMessage = createMessageSend(toStringAnnotation,
					lastAppendMessage, "append", createCharLiteral(')', toStringAnnotation));

		MessageSend toStringMessage = createMessageSend(toStringAnnotation,
					appendClosingParenthesesMessage, "toString");

		methodDeclaration.statements = new Statement[] {
					new ReturnStatement(toStringMessage, toStringAnnotation.sourceStart, toStringAnnotation.sourceEnd)
		};

		context.appendMethod(typeDeclaration, methodDeclaration);
		methodDeclaration.binding.tagBits |= ExtraCompilerModifiers.AccOverriding;
	}

	private static CharLiteral createCharLiteral(char c, ASTNode triggerNode) {
		char[] token = new char[] { '\'', c, '\'' };
		return new CharLiteral(token, triggerNode.sourceStart, triggerNode.sourceEnd);
	}

	private static StringLiteral createStringLiteral(String s, ASTNode triggerNode) {
		return createStringLiteral(s.toCharArray(), triggerNode);
	}

	private static StringLiteral createStringLiteral(char[] token, ASTNode triggerNode) {
		return new StringLiteral(token, triggerNode.sourceStart, triggerNode.sourceEnd, 0);
	}
	
	private static MessageSend createMessageSend(
			ASTNode triggerNode, Expression receiver, String selector, Expression... arguments) {
		MessageSend result = new MessageSend();
		result.sourceStart = triggerNode.sourceStart;
		result.sourceEnd = triggerNode.sourceEnd;
		result.receiver = receiver;
		result.selector = selector.toCharArray();
		result.arguments = arguments;
		return result;
	}
}
